﻿/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*             
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.server.functions.battles;

import com.chasekernan.hxnova.core.battles.BattlePlan;
import com.chasekernan.hxnova.core.components.BeamWeapon;
import com.chasekernan.hxnova.core.components.Component;
import com.chasekernan.hxnova.core.components.Missile;
import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.stars.Concentrations;
import com.chasekernan.hxnova.core.stars.Star;
import com.chasekernan.hxnova.utils.IntVector;
import com.chasekernan.hxnova.server.functions.battles.Battles;
import com.chasekernan.hxnova.core.stars.Concentrations;
import com.chasekernan.hxnova.core.battles.BattleRecord;

typedef BattleTargetInfo = {
    var primaryTarget : BattleTarget;
    var primaryScore : Float;
    
    var secondaryTarget : BattleTarget;
    var secondaryScore : Float;
};

typedef BattleTarget = { >BattleStack,
    var isPrimary : Bool;
    var distance : Int;
    var cost : Float;
}

typedef Damage = {
    var armor : Float;
    var shields : Float;
}

typedef BestMove = {
    var dx : Int;
    var dy : Int;
    var bestDamageTaken : Float;
    var bestDamageDone : Float;
    var bestValue : Float;
};

class BattleStack {
    
	//TODO: Add comments here (ironic i know)
    public var id : Int;
    public var parent : BattleParticipant;
    public var owner : Player;
    public var amount : Int;
    public var type : HullDesign;
    public var armor : Float;
    public var shields : Float;
    public var maxArmor : Float;
    public var maxShields : Float;
    public var battlePlan : BattlePlan;
    public var location : IntVector;
    public var baseSpeed : Float;
    public var fuel : Float;
    public var minerals : MineralSet;
    public var colonists : Int;
    public var targets : IntHash<BattleTarget>;
    public var battleMass : Float; //varies from round to round
    public var disengageCount : Int;
    public var isDestroyed : Bool;
    
    private var enemyStacks : Array<BattleTarget>;
    private var realWeight : Float;
    
    private function new() { }
    
    private static var ID_COUNT = 0;
    
    /**
        At the start of every battle, this needs to be called to clear the 
        start the id count at 0 again for stack creation.
    **/
    public static function clearIdCount() {
        ID_COUNT = 0;
    }
    
    public static function createFromShipStack(shipStack : ShipStack, 
                                               parent : BattleParticipant, 
                                               fleet : Fleet, 
                                               location : IntVector) 
                           : BattleStack {
        var battleStack = new BattleStack();
        
        battleStack.id = ID_COUNT++;
        battleStack.parent = parent;
        battleStack.owner = fleet.owner;
        battleStack.location = location.clone();
        battleStack.type = shipStack.type;
        battleStack.armor = cast shipStack.armor;
        battleStack.shields = cast shipStack.shields;
        battleStack.maxArmor = battleStack.armor;
        battleStack.maxShields = battleStack.shields;
        battleStack.battlePlan = shipStack.battlePlan;
        battleStack.baseSpeed = shipStack.type.getCombatSpeed();
        
        var cargo = fleet.removeShips(shipStack.type, shipStack.amount);
        battleStack.fuel = cargo.fuel;
        battleStack.minerals = cargo.minerals;
        battleStack.colonists = cargo.colonists;
        
        battleStack.realWeight = 
                shipStack.amount * battleStack.type.getTotalMass() +
                battleStack.minerals.getMass() + 
                battleStack.colonists / Fleet.COLONISTS_PER_KT;

        battleStack.targets = new IntHash();
        battleStack.battleMass = battleStack.realWeight;
        battleStack.disengageCount = Battles.MOVES_NEEDED_FOR_DISENGAGE;
        battleStack.isDestroyed = false;
        
        return battleStack;
    }
    
    public static function createFromStarbase(design : HullDesign,
                                              parent : BattleParticipant,
                                              star : Star,
                                              location : IntVector) 
                           : BattleStack {
        var battleStack = new BattleStack();
        
        battleStack.id = ID_COUNT++;
        battleStack.parent = parent;
        battleStack.owner = star.owner;
        battleStack.location = location.clone();
        battleStack.type = design;
        battleStack.armor = cast design.getArmor();
        battleStack.shields = cast design.getShields();
        battleStack.maxArmor = battleStack.armor;
        battleStack.maxShields = battleStack.shields;
        battleStack.battlePlan = BattlePlan.STARBASE_BATTLE_PLAN;
        battleStack.baseSpeed = 0;
        
        battleStack.fuel = 0;
        battleStack.minerals = new MineralSet();
        battleStack.colonists = 0;
        
        battleStack.targets = new IntHash();
        battleStack.realWeight = cast battleStack.type.getTotalMass();
        battleStack.battleMass = battleStack.realWeight;
        battleStack.disengageCount = -1;
        battleStack.isDestroyed = false;
        
        star.starbase = StarStarbase.None;
        
        return battleStack;
    }
    
    public function isStarbase() : Bool {
        return type.isStarbase();
    }
    
    /**
        Returns the weapon slots with their respective initiatives.
    **/
    public function getWeaponSlots() : Array<WeaponSlot> {
        var baseInitiative = 0;
        var weapons = new Array<WeaponSlot>();
        
        for (slot in type.slots) {
            if (!isWeapon(slot.component)) continue;
            
            var initiative = Reflect.field(slot.component, "initiative");
            weapons.push({slot : slot, parent : this, 
                          initiative : initiative + baseInitiative});
        }
        
        return weapons;
    }
    
    /**
        Returns how many squares the stack can move in the given round.
    **/
    public function getRoundMovement(round : Int) : Int {
        if (isStarbase()) return 0;
        
        /*
        http://wiki.gible.net/index.php?title=BATTLE.TXT
        MOVEMENT V/S SQUARES OF MOVEMENT PER ROUND TABLE

                            ROUND
        MOVEMENT      1     2    3    4    5    6    7    8
        -----------------------------------------------------
        1/2           1     0    1    0    1    0    1    0
        3/4           1     1    0    1    1    1    0    1
        1             1     1    1    1    1    1    1    1
        1 1/4         2     1    1    1    2    1    1    1
        1 1/2         2     1    2    1    2    1    2    1
        1 3/4         2     2    1    2    2    2    1    2
        2             2     2    2    2    2    2    2    2
        2 1/4         3     2    2    2    3    2    2    2
        2 1/2         3     2    3    2    3    2    3    2
        */
        
        //speed is in fourths, this turns it into int blocks of 4
        //ex speed of 1 3/4 -> intSpeed of 7
        var intSpeed = Std.int(Math.round(baseSpeed * 4));
        //[Std.int] rounds down
        return Std.int(baseSpeed) + switch(intSpeed % 4) {
            default: 0;
            case 1: if (round % 4 == 1) 1 else 0;
            case 2: if (round % 2 == 1) 1 else 0;
            case 3: if (round % 4 == 1) 0 else 1;
        }
    }

    /**
        Picks the best target for each weapon slot at this location.
    **/
    public function updateTargets() {
        if (!type.hasWeapons()) return;
        
        updateEnemyStacks();
        
        targets = new IntHash();
        
        for (i in 0...type.slots.length) {
            var slot = type.slots[i];
            if (!isWeapon(slot.component)) continue;
            
            targets.set(i, getTarget(slot).target);
        }
    }
    
    /**
        Returns true if the stack is out of range for this stack's weapons.
    **/
    public function isOutOfRange(stack : BattleStack) {
        var distance = location.getDistanceTo(stack.location);
        
        for (slot in type.slots) {
            
            var maxRange = switch (Type.getClass(slot.component)) {
                
                case untyped BeamWeapon: 
                    cast(slot, BeamWeapon).range;
                    
                case untyped Missile: 
                    cast(slot, Missile).range;
                    
                default: -1;
                
            };
            
            if (maxRange == -1) continue;
            if (distance <= maxRange) return false;
        }
        
        return true;
    }
    
    /**
        Returns the best overall target for the stack, without regard to weapon
        ranges.
        Returns null if no suitable target is found.
    **/
    public function getBestOverallTarget() : BattleStack {
        if (!type.hasWeapons()) return null;
        
        var targetInfo = getEmptyTarget();
        
        for (slot in type.slots) {
            if (!isWeapon(slot.component)) continue;
            
            var result = getTarget(slot, false);
            updateTargetInfo(targetInfo, result.target, result.attractiveness, 
                             result.target.isPrimary);
        }
        
        return if (targetInfo.primaryTarget != null) 
                    targetInfo.primaryTarget
               else 
                    targetInfo.secondaryTarget;
    }
    
    //returns the best target for the given slot
    public function getTarget(slot : DesignPart, ?checkRange = true) 
                    : { target : BattleTarget, attractiveness : Float } {
        var targetInfo = getEmptyTarget();
        
        for (stack in enemyStacks) {
            
            var distance = stack.distance;
            var maxRange = Reflect.field(slot.component, "range");
            if (distance > maxRange) {
                if (checkRange) return null;
                
                //cap at maxRange otherwise
                distance = maxRange;
            }
            
            var apn : Float = switch(Type.getClass(slot.component)) {
                
                case untyped BeamWeapon:
                    var bw = cast(slot.component, BeamWeapon);
                    var defence = if (bw.isSapper) 
                                        stack.shields 
                                  else 
                                        stack.shields + stack.armor;
                                        
                    if (maxRange != 0) {
                        defence *= 1.0 - 0.1 * distance / maxRange;
                    }
                    defence;
                    
                case untyped Missile:
                    var missile = cast(slot.component, Missile);
                    if (missile.accuracy < 0.001) {
                        throw "Missile accuracy cannot be 0.";
                    }
                    
                    var accuracy = 
                            Battles.getAccuracy(missile.accuracy, 
                                                type.getComputers());
                    
                    var weaponType = if (missile.isCapitalMissile) 2 else 1;
                    
                    var value = if (stack.shields >= stack.armor) 
                                    stack.armor * 2 / accuracy
                                else 
                                    stack.shields * 2 / accuracy +
                                    (stack.armor - stack.shields) / 
                                    (accuracy * weaponType);               
                    value;
            };
            
            //cost = resources + boranium
            var indivCost = type.getTotalCost();
            var cost = amount * (indivCost.resourceCost + 
                                 indivCost.mineralCost
                                 .getValue(Concentrations.MINERAL_TYPES[1]));
            var score = cost / apn;
            
            updateTargetInfo(targetInfo, stack, score, stack.isPrimary);
        }
        
        if (targetInfo.primaryTarget == null && 
            targetInfo.secondaryTarget == null) return null;
            
        return if (targetInfo.primaryTarget != null) 
                    { target : targetInfo.primaryTarget, 
                      attractiveness : targetInfo.primaryScore }
               else
                    { target : targetInfo.secondaryTarget, 
                      attractiveness : targetInfo.secondaryScore };
    }
    
    /**
        Returns true if this stack might target the given stack.
    **/
    public function willTarget(target : BattleStack) : Bool {
        for (stack in enemyStacks) {
            if (stack.id == target.id) return true;
        }
        
        return false;
    }
    
    /**
        Moves the stack one square and returns true if the stack has 
        disengaged.
    **/
    public function doMovement() : Bool {
        if (isStarbase()) return false;
        
        var overallTarget = getBestOverallTarget();
        
        if (overallTarget == null ) battlePlan.tactic = Disengage;
        else {
            if (isOutOfRange(cast overallTarget)) {
                
                //just move towards it
                var diffX = location.x - overallTarget.location.x;
                var diffY = location.y - overallTarget.location.y;
                
                location.x += if (diffX < 0) 1 
                              else if (diffX > 0) -1 
                              else 0;
                location.y += if (diffY < 0) 1 
                              else if (diffY > 0) -1
                              else 0;
                return false;
            }
        }
        
        var damageTaken = [ [0.0, 0.0, 0.0], 
                            [0.0, 0.0, 0.0], 
                            [0.0, 0.0, 0.0] ];
        var damageDone =  [ [0.0, 0.0, 0.0], 
                            [0.0, 0.0, 0.0], 
                            [0.0, 0.0, 0.0] ];
                            
        var oldLocation = location.clone();
        
        for (dx in -1...2) {
            for (dy in -1...2) {
                location = new IntVector(oldLocation.x + dx, 
                                         oldLocation.y + dy);
                                         
                //damage the other stacks can do to this stack at the new 
                //location
                for (stack in enemyStacks) {
                    if (!stack.willTarget(this)) continue;
                    
                    for (slot in stack.type.slots) {
                       if (!isWeapon(slot.component)) continue;
                            
                       var result = stack.getDamageDoneBySlot(slot, 
                                                              this, 
                                                              false);
                       damageTaken[dx + 1][dy + 1] += result.armor + 
                                                      result.shields;
                    }
                }
                
                //if disengaging, then this stack won't do any damage
                if (Type.enumEq(battlePlan.tactic, Disengage)) continue;
                
                //damage this stack can do to its targets at the given location.
                for (stack in enemyStacks) {
                    for (slot in type.slots) {
                        if (!isWeapon(slot.component)) continue;
                        
                        var targetInfo = getTarget(slot);
                        if (targetInfo == null || targetInfo.target == null) {
                            continue;
                        }
                        
                        var result = getDamageDoneBySlot(slot, 
                                                         targetInfo.target, 
                                                         false);
                        damageDone[dx + 1][dy + 1] = result.armor +
                                                     result.shields;
                    }
                }
            }
        }
        
        var bestMove = {dx : 0, dy : 0, bestDamageTaken : 1000000.0, 
                        bestDamageDone : 0.0, bestValue : 0.0};
        var thisStack = this; //so that the eval funcs can access this
        
        var evalFunc = switch (battlePlan.tactic) {
            
            case MinimizeDamageToSelf:
                function(dx : Int, dy : Int, taken : Float, done : Float) {
                    if (
                            //if the damage taken is the same, then the choose
                            // based on damage done
                            (Math.abs(taken - bestMove.bestDamageTaken) < 1 && 
                             done > bestMove.bestDamageDone)
                             
                             || 
                             
                             //otherwise just choose the least damage done
                            (taken < bestMove.bestDamageTaken)) {
                        
                        bestMove.dx = dx;
                        bestMove.dy = dy;
                        bestMove.bestDamageTaken = taken;
                        bestMove.bestDamageDone = done;
                    }
                }
                
            case MaximizeNetDamage:
                function(dx : Int, dy : Int, taken : Float, done : Float) {
                    var net = done - taken;
                    if (net > bestMove.bestValue) {
                        bestMove.dx = dx;
                        bestMove.dy = dy;
                        bestMove.bestValue = net;
                    }
                }
                
            case MaximizeDamageRatio:
                function(dx : Int, dy : Int, taken : Float, done : Float) {
                    if (taken < 1.0) taken = 1.0; //avoid a zero divide
                    
                    var ratio = done / taken;
                    if (ratio > bestMove.bestValue) {
                        bestMove.dx = dx;
                        bestMove.dy = dy;
                        bestMove.bestValue = ratio;
                    }
                }
            
            case MaximizeDamage:
                function(dx : Int, dy : Int, taken : Float, done : Float) {
                    if (
                            //if the damage done is the same, then the choose
                            // based on damage taken
                            (Math.abs(done - bestMove.bestDamageDone) < 1 && 
                             taken < bestMove.bestDamageTaken)
                             
                             || 
                             
                             //otherwise just choose the most damage done
                            (done > bestMove.bestDamageDone)) {
                        
                        bestMove.dx = dx;
                        bestMove.dy = dy;
                        bestMove.bestDamageTaken = taken;
                        bestMove.bestDamageDone = done;
                    }
                }
                
            case DisengageIfChallenged:
                function(dx : Int, dy : Int, taken : Float, done : Float) {
                    if (taken > 0.0) thisStack.battlePlan.tactic = Disengage;
                    else if (taken < 1.0) taken = 1.0; //avoid a zero divide
                    
                    var ratio = done / taken;
                    if (ratio > bestMove.bestValue) {
                        bestMove.dx = dx;
                        bestMove.dy = dy;
                        bestMove.bestValue = ratio;
                    }
                }
                
            case Disengage:
                function(dx : Int, dy : Int, taken : Float, done : Float) {
                    if (taken < bestMove.bestDamageTaken) {
                        bestMove.dx = dx;
                        bestMove.dy = dy;
                        bestMove.bestDamageTaken = taken;
                    }
                }
        }
        
        for (dx in -1...1) for (dy in -1...1) {
            evalFunc(dx, dy, 
                     damageTaken[dx + 1][dy + 1], 
                     damageDone[dx + 1][dy + 1]);
        }
        
        if (disengageCount <= 0) return true; //disengaged
        if (Type.enumEq(Disengage, battlePlan.tactic)) disengageCount--;
        
        location = new IntVector(oldLocation.x + bestMove.dx, 
                                 oldLocation.y + bestMove.dy);
        return false;
    }
    
    public function randomizeWeight() {
        battleMass = realWeight * 
                (1.0 + Math.random() * Battles.MASS_VARIANCE * 2 - 
                 Battles.MASS_VARIANCE);
    }
    
    /**
        Returns the armor left on a individual ship in this stack.
    **/
    public function getArmorOnIndividualShip() : Float {
        if (isStarbase()) return armor;
        
        var percent = armor / maxArmor;
        return type.getArmor() * percent;
    }
    
    /**
        Returns the total damage this weapon could do, without regard to the 
        armor the target has left or overflow (but it does care about shields).
        If useAccuracy is true, then this will apply accuracy, otherwise it's
        just the maximum possible damage returned
    **/
    public function getDamageDoneBySlot(slot : DesignPart, 
                                        target : BattleStack, 
                                        ?useAccuracy = true) 
                    : Damage {
        var armorDamage = 0.0;
        var shieldDamage = 0.0;
        
        switch (Type.getClass(slot.component)) {
            
            case untyped BeamWeapon:
                var bw = cast(slot, BeamWeapon);
                var range = location.getDistanceTo(target.location);
                var damage = Math.pow(1.0 - Battles.BEAM_DECAY_RATE, range) *
                             bw.power * slot.amount * amount;
                             
                shieldDamage = Math.min(damage, target.shields);
                if (!bw.isSapper && damage > shieldDamage) {
                    armorDamage = damage - shieldDamage; //allow overflow here.
                }
                
            case untyped Missile:
                var missile = cast(slot.component, Missile);
                var possibleDamage = missile.power * slot.amount * amount;
                
                var accuracy = Battles.getAccuracy(missile.accuracy, 
                                                   type.getComputers());
                if (useAccuracy && Math.random() > accuracy) {
                    
                    Battles.record.recordEvent(
                            BattleEvent.MissileMissed(this, slot, target));
                    
                    //missile missed so just do 1/8 damage to shields
                    shieldDamage = 
                            Math.min(target.shields, 
                                     Battles.MISSED_MISSILE_DAMAGE * 
                                     possibleDamage);
                } else {
                    if (target.shields < 0.000001 && missile.isCapitalMissile) {
                        armorDamage = 
                                Math.min(target.getArmorOnIndividualShip(),
                                         Battles.CAPITAL_MISSILE_MULTIPLIER * 
                                         possibleDamage);
                    } else {
                        var halfDamage = possibleDamage / 2.0;
                        
                        shieldDamage = Math.min(target.shields, halfDamage);
                        armorDamage = 
                                Math.min(target.getArmorOnIndividualShip(),
                                         halfDamage +
                                         if (halfDamage > shieldDamage) 
                                             halfDamage - shieldDamage
                                         else 0.0);
                    }
                }
        }
        
        return {armor : armorDamage, shields : shieldDamage};
    }
    
    public function updateEnemyStacks() {
        enemyStacks = new Array <BattleTarget>();
        
        for (stack in Battles.stacks) {
            if (stack.id == id) continue;
            
            var thisRelation = owner.relations.getRelationTo(stack.owner);
            var theirRelation = stack.owner.relations.getRelationTo(owner);
            
            if (!battlePlan.isBattleOpponent(thisRelation) &&
                !stack.battlePlan.isBattleOpponent(theirRelation)) continue;
                 
            var isPrimary = battlePlan.isPrimaryTarget(stack.type);
            var isSecondary = battlePlan.isSecondaryTarget(stack.type);
            if (!isPrimary && !isSecondary) continue;
            
            var asTarget : BattleTarget = cast stack;
            
            asTarget.isPrimary = isPrimary;
            asTarget.distance = location.getDistanceTo(stack.location);
            
            //taken from http://www.starsfaq.com/advfaq/guts2.htm#4.10
            var indivCost = stack.type.getTotalCost();
            //cost = resources + boranium
            asTarget.cost = stack.amount * 
                       (indivCost.resourceCost + 
                        indivCost.mineralCost.getValue(
                            Concentrations.MINERAL_TYPES[1]));
            
            enemyStacks.push(asTarget);
        }
    }
    
    private inline static function isWeapon(component : Component) : Bool {
        var c : Dynamic = untyped Type.getClass(component);
        return c == Missile || c == BeamWeapon;
    }
    
    //just returns an empty target info
    private static inline function getEmptyTarget() : BattleTargetInfo {
        return { primaryTarget : untyped null, primaryScore : 0.0,
                 secondaryTarget : untyped null, secondaryScore : 0.0 };
    }
    
    //updates target info with a new possible target
    private static inline function updateTargetInfo(info : BattleTargetInfo, 
                                                    target : BattleTarget, 
                                                    score : Float, 
                                                    isPrimary : Bool) {
        if (target != null) {
            if (isPrimary) {
                if (score > info.primaryScore) {
                    info.primaryScore = score;
                    info.primaryTarget = target;
                }
            } else {
                if (score > info.secondaryScore) {
                    info.secondaryScore = score;
                    info.secondaryTarget = target;
                }
            }
        }
    }
}